/*
 * View2d.java
 *
 * Created on Aug 18, 2010, 1:09:56 PM
 *
 * Copyright (c) 2010 Artur Rataj.
 *
 * This code is distributed under the terms of the GNU Library
 * General Public License, either version 2 of the license or, at
 * your option, any later version.
 */

package pl.gliwice.iitis.spatialnn.network.view;

import java.util.*;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;

import pl.gliwice.iitis.graphutils.Point;
import pl.gliwice.iitis.graphutils.view.*;
import pl.gliwice.iitis.spatialnn.network.*;

/**
 * Visualizes the state of a two--dimensional network.
 *
 * @author Artur Rataj
 */
public class NetworkView extends AbstractImageView {
    final double TEXT_SCALE = 1.2;
    /**
     * If only neurons and their connections should be drawn.
     */
    final boolean NEURONS_ONLY = false;
    
    /**
     * Default stroke width.
     */
    float strokeWidth;
    /**
     * Light blue color.
     */
    static Color lightBlue;
    /**
     * Dark green color.
     */
    static Color darkGreen;
    /**
     * Dark violet color.
     */
    static Color darkViolet;

    /**
     * Formats grid labels.
     */
    final protected DecimalFormat formatter = new DecimalFormat("#0.#");
    /**
     * A stroke for the grid.
     */
    BasicStroke gridDashedStroke;
    /**
     * A stroke for connections.
     */
    BasicStroke connectionStroke;
    /**
     * Scale of a neuron's view, default is 1.0.
     */
    float neuronScale;
    /**
     * If a neuron view should contain a textual description.
     */
    boolean neuronLegend;
    /**
     * If layers should have a legend; recommended if <code>neuronLegend</code> is false.
     */
    boolean layerLegend;

    static {
        lightBlue = new Color(0.6f, 0.8f, 1.0f, 1.0f);
        darkGreen = new Color(0.0f, 0.5f, 0.0f, 1.0f);
        darkViolet = new Color(0.5f, 0.0f, 0.8f, 1.0f);
    }
    
    /**
     * Creates a new instance of View.
     *
     * @param dm draw mode
     * @param width width of the produced images
     * @param neuronScale scale of a neuron's view, default is 1.0
     * @param neuronLegend if a neuron view should contain a textual description
     * @param layerLegend if layers should have a legend; recommended if
     * <code>neuronLegend</code> is false
     */
    public NetworkView(DrawMode dm, int width, float neuronScale, boolean neuronLegend,
            boolean layerLegend) {
        super(width, new DrawMode(dm.res == -1 ? 75 : dm.res, dm.bw));
        this.neuronScale = neuronScale;
        this.neuronLegend = neuronLegend;
        this.layerLegend = layerLegend;
        float m = this.dm.res/75.0f;
        strokeWidth = 1.0f*m;
        float[] dash1 = {
            2.0f*m,
            4.0f*m,
        };
        gridDashedStroke = new BasicStroke(strokeWidth,
                BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
                1.0f, dash1, 0.0f);
        float[] dash2 = {
            3.0f*m,
            3.0f*m,
        };
        connectionStroke = new BasicStroke(strokeWidth*2.0f*neuronScale,
                BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
                1.0f, dash2, 0.0f);
    }
    @Override
    public int getHeight(Drawable object) {
        Network network = (Network)object;
        int height = (int)Math.round(width*network.size.y/network.size.x);
        return height;
    }
    /**
     * Returns default tick step, for scale 0 ... <code>maxValue</code>.
     * 
     * @param maxValue                  maximum value on the scale
     * @return                          tick step
     */
    protected double getTickStep(int maxValue) {
        final int MULTIPLIER = 10;
        int tickStep = 1;
        while(maxValue*MULTIPLIER/tickStep > 7) {
            int i = tickStep;
            while(i%10 == 0)
                i /= 10;
            switch(i) {
                case 1:
                    i = 2;
                    break;

                case 2:
                    i = 5;
                    break;

                case 5:
                    i = 10;
                    break;

                default:
                    throw new RuntimeException("invalid value");
            }
            while(i < tickStep)
                i *= 10;
            tickStep = i;
        }
        return tickStep*1.0/MULTIPLIER;
    }
    public static Color getLayerColor(int layerNum) {
        switch(layerNum%4) {
            case 0:
                return Color.BLUE;

            case 1:
                return darkGreen;

            case 2:
                return darkViolet;
                
            case 3:
                return NeuronView.color;

            default:
                throw new RuntimeException("invalid index");
        }
    }
    /**
     * Draws connections in this network. Call this method before drawing
     * membranes or neurons.
     * 
     * @param g graphics object
     * @param network network, whose connections to draw
     * @param regionX left margin of the drawing area
     * @param regionY top margin of the drawing area
     * @param regionWidth width of the drawing area
     * @param regionHeight height of the drawing area
     */
    protected void drawConnections(Graphics2D g,
            Network network, int regionX,
            int regionY, int regionWidth, int regionHeight) {
        if(false)
            return;
        List<AbstractTarget> tBeg = new LinkedList<>();
        List<AbstractTarget> tEnd = new LinkedList<>();
        List<Integer> layerNum = new LinkedList<>();
        for(Neuron n : network.neurons) {
            AbstractTarget e = new NeuronTarget(n.key);
            for(AbstractTarget b : n.inputs) {
                tBeg.add(b);
                tEnd.add(e);
                layerNum.add(n.layerNum + 1);
            }
            // do not put inter--neuron connections twice
            for(AbstractTarget b : n.output)
                if(b instanceof MembraneTarget) {
                    tBeg.add(e);
                    tEnd.add(b);
                    layerNum.add(n.layerNum);
                }
        }
        Stroke prevStroke = g.getStroke();
        g.setStroke(connectionStroke);
        Iterator<AbstractTarget> bI = tBeg.iterator();
        Iterator<AbstractTarget> eI = tEnd.iterator();
        Iterator<Integer> lI = layerNum.iterator();
        while(bI.hasNext()) {
            AbstractTarget b = bI.next();
            AbstractTarget e = eI.next();
            Point locBeg = b.getLoc(network);
            Point locEnd = e.getLoc(network);
            if(!dm.bw)
                g.setColor(getLayerColor(lI.next()));
            g.drawLine((int)Math.round(regionX + locBeg.x*regionWidth/network.size.x),
                    (int)Math.round(regionY - locBeg.y*regionHeight/network.size.y),
                    (int)Math.round(regionX + locEnd.x*regionWidth/network.size.x),
                    (int)Math.round(regionY - locEnd.y*regionHeight/network.size.y));
        }
        g.setStroke(prevStroke);
        g.setColor(Color.BLACK);
    }
    /**
     * Defines one of the four margins around a rectangular region.
     */
    protected enum Margin {
        LEFT,
        RIGHT,
        TOP,
        BOTTOM,
    };
    /**
     * Draws values and ticks along an axis.
     *
     * @param g                         graphics of the rendered image
     * @param oX                        origin x coordinate
     * @param oY                        origin y coordinate
     * @param eX                        end x coordinate
     * @param eY                        end y coordinate
     * @param margin                    margin, in which the ticks are
     *                                  drawn
     * @param tickSize                  size of a tick in pixels
     * @param maxValue                  maximum value
     */
    protected void drawTicks(Graphics2D g, int oX, int oY,
            int eX, int eY, Margin margin, int tickSize, double maxValue) {
        g.setColor(Color.BLACK);
        double tickStep = getTickStep((int)maxValue);
        int tickExtendX = 0;
        int tickExtendY = 0;
        switch(margin) {
            case LEFT:
                tickExtendX = -tickSize;
                break;

            case RIGHT:
                tickExtendX = tickSize;
                break;

            case TOP:
                tickExtendY = -tickSize;
                break;

            case BOTTOM:
                tickExtendY = tickSize;
                break;

        }
        int ascent = g.getFontMetrics().getAscent();
        for(double i = 0; i <= maxValue; i += tickStep) {
            int px;
            int py;
            switch(margin) {
                case LEFT:
                case RIGHT:
                    px = oX;
                    py = eY + (int)Math.round((oY - eY)*i/maxValue);
                    break;

                case TOP:
                case BOTTOM:
                    px = oX + (int)Math.round((eX - oX)*i/maxValue);
                    py = oY;
                    break;

                default:
                    throw new RuntimeException("invalid margin");
            }
            g.drawLine(px, py, px + tickExtendX, py + tickExtendY);
            px += tickExtendX*3/2;
            py += tickExtendY*3/2;
            String text = "" + formatter.format(i);
            Rectangle2D bounds = getStringBounds(g, text);
            switch(margin) {
                case LEFT:
                    px -= bounds.getWidth();
                    py += ascent - bounds.getHeight()/2;
                    break;

                case RIGHT:
                    py += ascent - bounds.getHeight()/2;
                    break;

                case TOP:
                    px -= bounds.getWidth()/2;
                    break;

                case BOTTOM:
                    px -= bounds.getWidth()/2;
                    py += bounds.getHeight();
                    break;

            }
            g.drawString(text, px, py);
        }
    }
    /**
     * Draws axes around chart region, together with ticks and values.
     * 
     * @param g                         graphics of the rendered image
     * @param marginSize                margin size
     */
    protected void drawAxes(Network network, Graphics2D g, int marginSize) {
        int height = getHeight(network);
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(strokeWidth));
        g.drawRect(marginSize, marginSize,
                width - 2*marginSize, height - 2*marginSize);
        int tickSize = marginSize/5;
        drawTicks(g,
                marginSize, marginSize,
                marginSize, height - marginSize,
                Margin.LEFT, tickSize, network.size.y);
        drawTicks(g,
                width - marginSize, marginSize,
                width - marginSize, height - marginSize,
                Margin.RIGHT, tickSize, network.size.y);
        drawTicks(g,
                marginSize, marginSize,
                width - marginSize, marginSize,
                Margin.TOP, tickSize, network.size.x);
        drawTicks(g,
                marginSize, height - marginSize,
                width - marginSize, height - marginSize,
                Margin.BOTTOM, tickSize, network.size.x);
    }
    /**
     * Draws grid over chart region.
     *
     * @param g                         graphics of the rendered image
     * @param oX                        origin x coordinate
     * @param oY                        origin y coordinate
     * @param eX                        end x coordinate
     * @param eY                        end y coordinate
     * @param maxValueX                 maximum value x
     * @param maxValueY                 maximum value y
     */
    protected void drawGrid(Graphics2D g, int oX, int oY,
            int eX, int eY, double maxValueX, double maxValueY) {
        Stroke prevStroke = g.getStroke();
        g.setColor(Color.BLACK);
        g.setStroke(gridDashedStroke);
        double tickStepX = getTickStep((int)Math.round(maxValueX));
        double tickStepY = getTickStep((int)Math.round(maxValueY));
        for(double i = tickStepX; i < maxValueX; i += tickStepX) {
            int px = oX + (int)Math.round((eX - oX)*i/maxValueX);
            g.drawLine(px, oY, px, eY);
        }
        for(double i = tickStepY; i < maxValueY; i += tickStepY) {
            int py = eY + (int)Math.round((oY - eY)*i/maxValueY);
            g.drawLine(oX, py, eX, py);
        }
        g.setStroke(prevStroke);
    }
    /**
     * Renders a frame representing the current state of the network.
     *
     * @return                          rendered image
     */
    @Override
    public void draw(Drawable object, int x, int y, Graphics2D g) {
        Network network = (Network)object;
        int height = getHeight(object);
        int minDim = Math.min(width, height);
        float m = dm.res/75.0f;
        int marginSize;
        if(NEURONS_ONLY)
            marginSize = 0;
        else
            marginSize = (int)Math.round(30*m*TEXT_SCALE);
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        Object fmObject = g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS);
        Object scObject = g.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_NORMALIZE);
        g.setFont(new Font("dialog", Font.PLAIN, (int)Math.round(12*TEXT_SCALE*m)));
        if(!NEURONS_ONLY) {
            drawAxes(network, g, marginSize);
            drawGrid(g,
                    marginSize, marginSize,
                    width - marginSize, height - marginSize,
                    network.size.x, network.size.y);
        }
        g.setColor(Color.BLACK);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                fmObject);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                scObject);
        int regionX = marginSize;
        int regionY = height - marginSize;
        int regionWidth = (width - marginSize*2);
        int regionHeight = (height - marginSize*2);
        MembraneView mv = new MembraneView(dm,
                new Point(marginSize, height - marginSize),
                new Point(width - marginSize, marginSize));
        Point membraneMin = new Point(0, 0);
        Point membraneMax = new Point(network.size);
        drawConnections(g, network, regionX, regionY,
                regionWidth, regionHeight);
        if(!NEURONS_ONLY)
            for(Membrane membrane : network.membranes)
                mv.draw(membrane, membraneMin, membraneMax, g);
        NeuronView cv = new NeuronView(dm, neuronScale, neuronLegend);
        g.setStroke(new BasicStroke(strokeWidth));
        if(network.turnBound != null) {
            g.setColor(Color.GRAY);
            g.drawRect((int)Math.round(regionX + network.turnBound.min.x*regionWidth/network.size.x),
                    (int)Math.round(regionY - network.turnBound.max.y*regionHeight/network.size.y),
                    (int)Math.round((network.turnBound.max.x - network.turnBound.min.x)*regionWidth/network.size.x),
                    (int)Math.round((network.turnBound.max.y - network.turnBound.min.y)*regionHeight/network.size.y));
            g.setColor(Color.BLACK);
        }
        SortedMap<Integer, Neuron> layers = new TreeMap<>();
        for(Neuron n : network.neurons) {
            layers.put(n.layerNum, n);
            Point at = new Point(
                    regionX + n.loc.x*regionWidth/network.size.x,
                    regionY - n.loc.y*regionHeight/network.size.y);
            cv.draw(n, at, g);
        }
        if(!NEURONS_ONLY) {
            String text = "layer 00, type TYPE";
            Rectangle2D bounds = getStringBounds(g, text);
            int ascent = g.getFontMetrics().getAscent();
            double lineHeight = Math.max(bounds.getHeight(),
                    cv.RADIUS*cv.dm.res*2*1.3);
            for(int layer : layers.keySet()) {
                Neuron n = layers.get(layer);
                Point at = new Point(
                        regionX + regionWidth - bounds.getWidth(),
                        regionY - lineHeight*(layer + 1));
                text = "layer " + layer + ", type " + n.type;
                g.setColor(Color.BLACK);
                g.drawString(text, (int)at.x, (int)at.y + ascent/2);
                at.x -= lineHeight*0.8;
                cv.draw(n, at, g);
            }
        }
    }
    public BufferedImage render(Network network) {
        int mode;
        if(dm.bw)
            mode = BufferedImage.TYPE_BYTE_GRAY;
        else
            mode = BufferedImage.TYPE_3BYTE_BGR;
        BufferedImage out = new BufferedImage(width, getHeight(network),
                mode);
        Graphics2D g = out.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_PURE);
        draw(network, 0, 0, g);
        return out;
    }
}
